package com.geor.gcv.core.mat;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;

import org.opencv.core.Mat;
import org.slf4j.Logger;

import com.geor.common.log.Loggers;
import com.geor.gcv.core.model.Area;
import com.geor.gcv.core.model.SharpArea;

/**
 * 瀑布法字符分割器实现
 * 
 * @author geor.yuli
 * 
 */
public class CellChipperFallImpl implements CellChipper {

	static Logger log = Loggers.get();

	/** 最大循环次数防止死循环 */
	static int MAX_LOOP = 100;

	@Override
	public List<Area> execute(Mat mat) throws Exception {
		log.trace(String.format("input mat: {rows: %d, cols: %d}", mat.rows(), mat.cols()));
		List<Area> areas = new ArrayList<Area>(30);
		Mat canvas = mat.clone();
		try {
			int topY = findPeakTop(canvas) - 1;
			int findDeep = canvas.rows() * 1 / 2;
			for (int i = topY; i >= 0 && i < findDeep; i++) {
				int[][] peaks = findPeaks(canvas, areas, i);

				for (int j = 0; j < peaks.length; j++) {
					if (peaks[j][0] == 0)
						break;

					// 跳过已切分的区域
					if (contain(peaks[j][0], areas))
						continue;

					int[] thisLeftPoint = drip(canvas, peaks[j][0], peaks[j][1] - 1, false);
					int[] thisRightPoint = drip(canvas, peaks[j][0], peaks[j][1] - 1, true);

					if (thisRightPoint[0] - thisLeftPoint[0] > 0) {
						log.trace(String.format("find bound: [left: %d, right: %d]", thisLeftPoint[0],
								thisRightPoint[0]));
						int bottom = findBottom(canvas, thisLeftPoint[0], thisRightPoint[0]);

						SharpArea area = new SharpArea(thisLeftPoint[0], peaks[j][1],
								thisRightPoint[0] - thisLeftPoint[0], bottom - peaks[j][1] + 1);
						area.setP1(thisLeftPoint);
						area.setP2(thisRightPoint);
						areas.add(area);
					}
				}
			}

			// 区域重新排序
			Collections.sort(areas, new Comparator<Area>() {
				@Override
				public int compare(Area a1, Area a2) {
					return a1.getX() - a2.getX();
				}
			});
			cover(areas);
			depart(canvas, areas);
			return areas;
		} finally {
			Mats.releseMat(canvas);
		}
	}

	@Override
	public void setParams(Properties properties) {
	}

	// 滴水式获取分割线列坐标
	private int[] drip(Mat mat, int x, int y, boolean turnRight) {
		int[] keyPoint = new int[2];
		keyPoint[0] = x;
		keyPoint[1] = y;
		// 保留的顶部高度
		int KEEP_TOP = 2;
		// 保留的底部高度
		// int KEEP_BOT = mat.rows() / 4;
		// 最少步数
		int MIN_STEP = 1;
		int thisX = x;
		int thisY = y;
		int cliff = y;
		for (int r = 0; r < MAX_LOOP; r++) {
			// 边界
			if (thisX < 0 || thisX >= mat.cols() - 1) {
				break;
			}
			if (thisY < 0 || thisY >= mat.rows() - 1) {
				break;
			}

			double[] matD = mat.get(thisY + 1, thisX);
			// 向下试探遇到障碍
			if (null != matD && matD[0] == 255d) {
				// 向左还是向右
				thisX = (turnRight) ? thisX + 1 : thisX - 1;
				cliff = thisY;
				// 有障碍则是死路退出, 过滤顶部毛刺的影响
				if (r > MIN_STEP && Math.abs(thisY - y) > KEEP_TOP && mat.get(thisY, thisX)[0] == 255d) {
					// 退出前回撤
					thisX = (turnRight) ? thisX - 1 : thisX + 1;
					cliff = thisY;
					break;
				}
			}
			// 如果没有障碍则继续下探
			else {
				thisY++;
			}
		}
		keyPoint[0] = Math.max(0, thisX);
		keyPoint[1] = Math.max(0, cliff);
		return keyPoint;
	}


	/**
	 * 找到一行中的一个或多个峰顶
	 * 
	 * @param mat
	 * @param areas
	 * @param row
	 * @return
	 */
	private int[][] findPeaks(Mat mat, List<Area> areas, int row) {
		int[] availablePoints = new int[mat.cols()];
		// 跳过已有的区域
		for (Area area : areas) {
			for (int i = area.getX(); i < area.getX() + area.getWidth(); i++) {
				availablePoints[i] = -1;
			}
		}
		// 全部的峰顶坐标
		int[][] peaks = new int[mat.cols()][2];
		int peakIndex = 0;
		// 一段连续的峰顶的起始和末尾
		int topBegin = 0;
		int topEnd = 0;
		for (int i = 0; i < availablePoints.length; i++) {
			if (availablePoints[i] == -1)
				continue;
			// 点
			if (mat.get(row, i)[0] == 255d) {
				// 距离上一个峰顶末尾
				if (topBegin == 0) {
					topBegin = i;
				}
				// 连续
				else {
					topEnd = i;
					// 到达右侧边界则峰顶结束
					if (topEnd == mat.cols() - 1) {
						peaks[peakIndex][1] = row;
						peaks[peakIndex][0] = (topBegin + topEnd) / 2;
						log.trace(String.format("find top: [%d, %d]", peaks[peakIndex][0], peaks[peakIndex][1]));
						topBegin = 0;
						topEnd = 0;
					}
				}
				// 非点并且峰顶起始不为0表示一段峰顶结束
			} else if (topBegin > 0) {
				topEnd = i;
				peaks[peakIndex][1] = row;
				peaks[peakIndex][0] = (topBegin + topEnd) / 2;
				log.trace(String.format("find top: [%d, %d]", peaks[peakIndex][0], peaks[peakIndex][1]));
				topBegin = 0;
				topEnd = 0;
				peakIndex++;
			}
		}
		return peaks;
	}

	/**
	 * 找到Mat峰顶Y坐标, 去除顶上的点干扰
	 * 
	 * @param mat
	 * @return
	 */
	private int findPeakTop(Mat mat) {
		int[] horizons = new int[mat.rows()];
		for (int i = 0; i < mat.rows(); i++) {
			int total = 0;
			for (int j = 0; j < mat.cols(); j++) {
				if (mat.get(i, j)[0] == 255d) {
					total++;
				}
			}
			horizons[i] = total;
		}
		int index = 0;
		int height = 0;
		for (int i = 0; i < horizons.length; i++) {
			if (horizons[i] > 0) {
				height++;
				index = i;
			} else {
				height = 0;
			}
			if (height >= Math.min(mat.rows(), 5)) {
				return index + 1 - height;
			}
		}
		return 0;
	}

	/**
	 * 找到一个区间的底部
	 * 
	 * @param mat
	 * @param left
	 * @param right
	 * @return
	 */
	private int findBottom(Mat mat, int left, int right) {
		int[] horizons = new int[mat.rows()];
		for (int i = 0; i < mat.rows(); i++) {
			int total = 0;
			for (int j = left; j < right; j++) {
				if (mat.get(i, j)[0] == 255) {
					total++;
				}
			}
			horizons[i] = total;
		}
		int index = 0;
		int height = 0;
		for (int i = horizons.length - 1; i > 0; i--) {
			if (horizons[i] > 0) {
				height++;
				index = i;
			} else {
				height = 0;
			}
			// Y轴投影连续超过5次
			if (height >= Math.min(mat.rows(), 3)) {
				return index + height;
			}
		}
		return mat.rows() - 1;
	}

	/**
	 * 调整相邻区域之间的层叠交集
	 * 
	 * @param areas
	 */
	private void cover(List<Area> areas) {
		for (int i = 1; i < areas.size(); i++) {
			SharpArea thisArea = (SharpArea) areas.get(i);
			SharpArea prevArea = (SharpArea) areas.get(i - 1);

			if (null != thisArea.getP1() && null != prevArea.getP2()) {
				// 右侧区域的左侧切分X 小于 左侧区域的右侧切分X, 说明两个区域之间有交集
				if (thisArea.getP1()[0] < prevArea.getP2()[0]) {

					// TODO 暂时只处理左侧区域遮盖右侧区域, 右侧区域覆盖左侧区域比较少暂时先不处理
					prevArea.setP2(thisArea.getP1());
					prevArea.setWidth(thisArea.getP1()[0] - prevArea.getX() + 1);

					
					// 右侧区域宽度较小 
//					if (thisArea.getWidth() < prevArea.getWidth()) {
//						prevArea.setP2(thisArea.getP1());
//						prevArea.setWidth(thisArea.getP1()[0] - prevArea.getX() + 1); 
//					} 
					// 左侧区域宽度较小 
//					else if (thisArea.getWidth() < prevArea.getWidth()) { 
//						thisArea.setP1(prevArea.getP2());
//						thisArea.setWidth(thisArea.getP2()[0] - prevArea.getP2()[0] + 1); 
//					}
				}
			}
		}
	}

	/**
	 * 切分粘连区域
	 * 
	 * @param areas
	 * @return
	 */
	private void depart(Mat mat, List<Area> areas) {

		// 计算已有区域的平均宽度
		int totalWidth = 0;
		int counter = 0;
		for (Area area : areas) {
			if (((SharpArea) area).scale < 1) {
				counter++;
				totalWidth += area.getWidth();
			}
		}
		// 正常区域的平均宽度
		float areaAvgWidth = (float) totalWidth / (float) counter;
		log.trace(String.format("total width: %d, avg width: %f", totalWidth, areaAvgWidth));

		// 忽略非正常区域
		if (areaAvgWidth < 3) {
			return;
		}

		// 超宽区域处理
		for (int i = 0; i < areas.size(); i++) {
			SharpArea area = (SharpArea) areas.get(i);
			if (area.scale >= 1) {
				List<SharpArea> children = new ArrayList<SharpArea>();
				int childnum = Math.round((float) area.getWidth() / areaAvgWidth);
				if (childnum == 0)
					continue;
				int childWidth = area.getWidth() / childnum;
				log.trace(String.format("[%d] {num: %d, width: %d}", i, childnum, childWidth));
				int offset = area.getX();
				for (int j = 0; j < childnum; j++) {
					SharpArea childArea = new SharpArea(offset, area.getY(), childWidth, area.getHeight());
					log.trace(String.format("child: {x: %d, width: %d}", childArea.getX(), childArea.getWidth()));
					offset += childArea.getWidth() + 1;
					children.add(childArea);
				}
				areas.remove(i);
				areas.addAll(i, children);
				children.clear();
			}
		}
	}

	/**
	 * 一个点的X坐标是否包含在区间集合中
	 * 
	 * @param point
	 * @param areas
	 * @return
	 */
	private boolean contain(int x, List<Area> areas) {
		for (Area area : areas) {
			if (x >= area.getX() && x <= area.getX() + area.getWidth()) {
				return true;
			}
		}
		return false;
	}
}
